package bancosys.tec.persist.hibernate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Restrictions;

import bancosys.tec.persist.Persistable;
import bancosys.tec.utils.date.Date;

/**
 * Restrictions Adder.<br>
 * Adiciona restrictions à criteria, caso o objeto passado seja não nulo.
 * 
 * @created 04/09/2008
 */
public final class RestrictionsUtils {

    private static final Log LOG = LogFactory.getLog(RestrictionsUtils.class);

    private static final int IN_MAX_SIZE = 999;

    private static final int IN_WARN_THRESHOLD = 100;

    /**
     * Construtor
     */
    private RestrictionsUtils() {
        super();
    }

    /**
     * Adiciona uma restriction EQUALS à criteria, caso o objeto seja não nulo.
     * 
     * @param criteria a criteria.
     * @param objeto um objeto java.
     * @param propertyName a propertyName.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionEq(Criteria criteria, String propertyName, Object objeto) {
        if (objeto != null) {
            criteria.add(Restrictions.eq(propertyName, objeto));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction EQUALS à detachedCriteria, caso o objeto seja não nulo.
     * 
     * @param criteria a detachedCriteria.
     * @param objeto um objeto java.
     * @param propertyName a propertyName.
     * @return a criteriaAdd.
     */
    public static DetachedCriteria addRestrictionEq(DetachedCriteria criteria, String propertyName, Object objeto) {
        if (objeto != null) {
            criteria.add(Restrictions.eq(propertyName, objeto));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction EQUALS à criteria, caso o objeto seja não nulo.
     * 
     * @param criteria a criteria.
     * @param bo um bo.
     * @param propertyName a propertyName.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionEqId(Criteria criteria, String propertyName, Persistable bo) {
        if (bo != null) {
            criteria.add(Restrictions.eq(propertyName, bo.getPk()));
        }
        return criteria;

    }

    /**
     * Adiciona uma restriction GREATER OR EQUALS à criteria, caso o objeto seja não nulo.
     * 
     * @param criteria a criteria.
     * @param objeto um objeto java.
     * @param propertyName a propertyName.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionGe(Criteria criteria, String propertyName, Object objeto) {
        if (objeto != null) {
            criteria.add(Restrictions.ge(propertyName, objeto));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction LESS OR EQUALS à criteria, caso o objeto seja não nulo.
     * 
     * @param criteria a criteria.
     * @param objeto um objeto java.
     * @param propertyName a propertyName.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionLe(Criteria criteria, String propertyName, Object objeto) {
        if (objeto != null) {
            criteria.add(Restrictions.le(propertyName, objeto));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction BETWEEN à criteria, caso ambas as datas limites sejam não nulas.
     * 
     * @param criteria a criteria.
     * @param beginningDate a data inicial.
     * @param enddingDate a data final.
     * @param propertyName a propertyName.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionBetween(Criteria criteria, String propertyName, Date beginningDate, Date enddingDate) {
        if (beginningDate != null && enddingDate != null) {
            criteria.add(Restrictions.between(propertyName, beginningDate, enddingDate));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction LIKE à criteria, caso o objeto não seja nulo.
     * 
     * @param criteria a criteria.
     * @param propertyName a propertyName.
     * @param object um objeto.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionLike(Criteria criteria, String propertyName, Object object) {
        if (object != null) {
            criteria.add(Restrictions.like(propertyName, object));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction LIKE à criteria com um tipo de {@link MatchMode}, caso a string não seja nula.
     * 
     * @param criteria a criteria.
     * @param propertyName a propertyName.
     * @param value uma string.
     * @param matchMode a matchMode.
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionLike(Criteria criteria, String propertyName, String value, MatchMode matchMode) {
        if (value != null && matchMode != null) {
            criteria.add(Restrictions.like(propertyName, value, matchMode));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction in à criteria, caso o objeto não seja nulo.
     * 
     * @param criteria a criteria.
     * @param propertyName a propertyName.
     * @param object um objeto java (vetor).
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionIn(Criteria criteria, String propertyName, Object[] object) {
        if (object != null && object.length > 0) {
            logInCall(object.length);
            criteria.add(createSafeIn(propertyName, Arrays.asList(object)));
        }
        return criteria;

    }

    /**
     * Adiciona uma restriction in à criteria, caso o objeto não seja nulo.
     * 
     * @param criteria a criteria.
     * @param propertyName a propertyName.
     * @param object um objeto java (Collection).
     * @return a criteriaAdd.
     */
    public static Criteria addRestrictionIn(Criteria criteria, String propertyName, Collection<? extends Object> object) {
        if (object != null && !object.isEmpty()) {
            logInCall(object.size());
            criteria.add(createSafeIn(propertyName, new ArrayList<Object>(object)));
        }
        return criteria;
    }

    /**
     * Adiciona uma restriction in à criteria, caso o objeto não seja nulo.
     * 
     * @param criteria a criteria.
     * @param propertyName a propertyName.
     * @param object um objeto java (vetor).
     * @return a criteriaAdd.
     */
    public static DetachedCriteria addRestrictionIn(DetachedCriteria criteria, String propertyName, Object[] object) {
        if (object != null && object.length > 0) {
            logInCall(object.length);
            criteria.add(createSafeIn(propertyName, Arrays.asList(object)));
        }
        return criteria;

    }

    /**
     * Adiciona uma restriction in à criteria, caso o objeto não seja nulo.
     * 
     * @param criteria a criteria.
     * @param propertyName a propertyName.
     * @param object um objeto java (Collection).
     * @return a criteriaAdd.
     */
    public static DetachedCriteria addRestrictionIn(DetachedCriteria criteria, String propertyName, Collection<? extends Object> object) {
        if (object != null && !object.isEmpty()) {
            logInCall(object.size());
            criteria.add(createSafeIn(propertyName, new ArrayList<Object>(object)));
        }
        return criteria;

    }

    /**
     * @param propertyName a propertyName
     * @param objects objects
     * @return a {@link Criterion}
     */
    private static Criterion createSafeIn(String propertyName, List<?> objects) {
        if (objects.size() <= IN_MAX_SIZE) {
            return Restrictions.in(propertyName, objects);
        } else {
            return Restrictions.or(createSafeIn(propertyName, objects.subList(0, IN_MAX_SIZE)),
                    createSafeIn(propertyName, objects.subList(IN_MAX_SIZE, objects.size())));
        }
    }

    /**
     * Logs a call to an in restriction.
     * 
     * @param inSize inSize
     */
    private static void logInCall(int inSize) {
        if (inSize <= IN_WARN_THRESHOLD) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Restriction IN called from %s with %s elements.", getCallerDescription(), inSize));
            }
        } else {
            if (LOG.isWarnEnabled()) {
                LOG.warn(String.format("Restriction IN called from %s with %s elements (TOO MANY).", getCallerDescription(), inSize));
            }
        }
    }

    /**
     * Obtains the stack trace element description of the method that called the DAO that used the IN Restriction.
     * 
     * @return description
     */
    private static String getCallerDescription() {
        Throwable t = new Throwable();
        for (StackTraceElement element : t.getStackTrace()) {
            String stackClass = element.getClassName();
            if (!stackClass.endsWith("DAO") && !stackClass.equals(RestrictionsUtils.class.getName())) {
                return element.toString();
            }
        }
        return "UNKNOWN";
    }
}
